using UnityEngine;

[RequireComponent(typeof(Camera))]
public class OrbitCamera : MonoBehaviour
{
    // 对焦位置
    [SerializeField]
    Transform focus = default;

    // 距离
    [SerializeField, Range(1f, 20f)]
    float distance = 5f;
    
    // 聚焦半径
    [SerializeField, Min(0f)]
    float focusRadius = 1f;
    
    // 焦点中心因子
    [SerializeField, Range(0f, 1f)]
    float focusCentering = 0.5f;
    
    // 旋转速度
    [SerializeField, Range(1f, 360f)]
    float rotationSpeed = 90f;
    
    // 限制垂直方向的视角
    [SerializeField, Range(-89f, 89f)]
    float minVerticalAngle = -30f, maxVerticalAngle = 60f;
    
    // 延迟对焦
    [SerializeField, Min(0f)]
    float alignDelay = 5f;
    
    // 对齐平滑
    [SerializeField, Range(0f, 90f)]
    float alignSmoothRange = 45f;
    
    // 遮挡掩码
    [SerializeField]
    LayerMask obstructionMask = -1;
    
    // 焦点；上一帧的焦点
    Vector3 focusPoint, previousFocusPoint;
    
    // 摄像机角度
    Vector2 orbitAngles = new Vector2(45f, 0f);
    
    // 上次手动旋转的时间
    float lastManualRotationTime;
    
    // 摄像机
    Camera regularCamera;
    
    // 重力对齐
    Quaternion gravityAlignment = Quaternion.identity;
    
    Quaternion orbitRotation;

    // 摄像机的半个拓展
    Vector3 CameraHalfExtends {
        get {
            Vector3 halfExtends;
            halfExtends.y =
                regularCamera.nearClipPlane *
                Mathf.Tan(0.5f * Mathf.Deg2Rad * regularCamera.fieldOfView);
            halfExtends.x = halfExtends.y * regularCamera.aspect;
            halfExtends.z = 0f;
            return halfExtends;
        }
    }
    
    /// <summary>
    /// 进行检测
    /// </summary>
    void OnValidate () {
        if (maxVerticalAngle < minVerticalAngle) {
            maxVerticalAngle = minVerticalAngle;
        }
    }
    
    void Awake () {
        regularCamera = GetComponent<Camera>();
        focusPoint = focus.position;
        transform.localRotation = Quaternion.Euler(orbitAngles);
    }
    
    void LateUpdate () 
    {
        gravityAlignment =
            Quaternion.FromToRotation(
                gravityAlignment * Vector3.up, -Physics.gravity.normalized
            ) * gravityAlignment;
        UpdateFocusPoint();
        Quaternion lookRotation;
        if (ManualRotation() || AutomaticRotation()) {
            ConstrainAngles();
            lookRotation = Quaternion.Euler(orbitAngles);
        }
        else {
            lookRotation = transform.localRotation;
        }
        Vector3 lookDirection = lookRotation * Vector3.forward;
        Vector3 lookPosition = focusPoint - lookDirection * distance;
        
        Vector3 rectOffset = lookDirection * regularCamera.nearClipPlane;
        Vector3 rectPosition = lookPosition + rectOffset;
        Vector3 castFrom = focus.position;
        Vector3 castLine = rectPosition - castFrom;
        float castDistance = castLine.magnitude;
        Vector3 castDirection = castLine / castDistance;
        
        if (Physics.BoxCast(castFrom, CameraHalfExtends, castDirection, 
                out RaycastHit hit, lookRotation, castDistance, obstructionMask)) 
        {
            rectPosition = castFrom + castDirection * hit.distance;
            lookPosition = rectPosition - rectOffset;
        }
        
        transform.SetPositionAndRotation(lookPosition, lookRotation);
    }
    
    /// <summary>
    /// 更新焦点
    /// </summary>
    void UpdateFocusPoint () 
    {
        previousFocusPoint = focusPoint;
        Vector3 targetPoint = focus.position;
        if (focusRadius > 0f) 
        {
            float distance = Vector3.Distance(targetPoint, focusPoint);
            float t = 1f;
            if (distance > 0.01f && focusCentering > 0f) {
                t = Mathf.Pow(1f - focusCentering, Time.unscaledDeltaTime);
            }
            if (distance > focusRadius) {
                t = Mathf.Min(t, focusRadius / distance);
            }
            focusPoint = Vector3.Lerp(targetPoint, focusPoint, t);
        }
        else 
        {
            focusPoint = targetPoint;
        }
    }
    
    /// <summary>
    /// 手动旋转 -- 返回是否成功调整
    /// </summary>
    bool ManualRotation () {
        Vector2 input = new Vector2(
            Input.GetAxis("Vertical Camera"),
            Input.GetAxis("Horizontal Camera")
        );
        const float e = 0.001f;
        if (input.x < -e || input.x > e || input.y < -e || input.y > e) {
            orbitAngles += rotationSpeed * Time.unscaledDeltaTime * input;
            lastManualRotationTime = Time.unscaledTime;
            return true;
        }
        return false;
    }
    
    /// <summary>
    /// 限制摄像机视角
    /// </summary>
    void ConstrainAngles () 
    {
        orbitAngles.x =
            Mathf.Clamp(orbitAngles.x, minVerticalAngle, maxVerticalAngle);

        if (orbitAngles.y < 0f) {
            orbitAngles.y += 360f;
        }
        else if (orbitAngles.y >= 360f) {
            orbitAngles.y -= 360f;
        }
    }
    
    /// <summary>
    /// 自动旋转
    /// </summary>
    bool AutomaticRotation () 
    {
        if (Time.unscaledTime - lastManualRotationTime < alignDelay) {
            return false;
        }
        
        Vector2 movement = new Vector2(
            focusPoint.x - previousFocusPoint.x,
            focusPoint.z - previousFocusPoint.z
        );
        float movementDeltaSqr = movement.sqrMagnitude;
        if (movementDeltaSqr < 0.0001f) {
            return false;
        }
        float headingAngle = GetAngle(movement / Mathf.Sqrt(movementDeltaSqr));
        float deltaAbs = Mathf.Abs(Mathf.DeltaAngle(orbitAngles.y, headingAngle));
        float rotationChange = rotationSpeed *  Mathf.Min(Time.unscaledDeltaTime, movementDeltaSqr);
        if (deltaAbs < alignSmoothRange) {
            rotationChange *= deltaAbs / alignSmoothRange;
        }
        else if (180f - deltaAbs < alignSmoothRange) {
            rotationChange *= (180f - deltaAbs) / alignSmoothRange;
        }
        orbitAngles.y = Mathf.MoveTowardsAngle(orbitAngles.y, headingAngle, rotationChange);
        return true;
    }
    
    /// <summary>
    /// 二维方向转变为角度
    /// </summary>
    /// <param name="direction"></param>
    /// <returns></returns>
    static float GetAngle (Vector2 direction) {
        float angle = Mathf.Acos(direction.y) * Mathf.Rad2Deg;
        return direction.x < 0f ? 360f - angle : angle;
    }
}